package org.zxt.web.nio;

import java.io.*;
import java.net.Socket;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

public abstract class NioHTTPD {

    public static final int SOCKET_READ_TIMEOUT = 5000;

    public static final String MIME_PLAINTEXT = "text/plain";
    public static final String MIME_APPLICATION_JSON = "application/json; charset=utf-8";

    public static final String MIME_HTML = "text/html";

    private static final String QUERY_STRING_PARAMETER = "NanoHttpd.QUERY_STRING";

    static final Logger LOG = Logger.getLogger(NioHTTPD.class.getName());

    protected static Map<String, String> MIME_TYPES;

    public static Map<String, String> mimeTypes() {
        if (MIME_TYPES == null) {
            MIME_TYPES = new HashMap<String, String>();
            loadMimeTypes(MIME_TYPES, "META-INF/nanohttpd/default-mimetypes.properties");
            loadMimeTypes(MIME_TYPES, "META-INF/nanohttpd/mimetypes.properties");
            if (MIME_TYPES.isEmpty()) {
                LOG.log(Level.WARNING, "no mime types found in the classpath! please provide mimetypes.properties");
            }
        }
        return MIME_TYPES;
    }

    @SuppressWarnings({
            "unchecked",
            "rawtypes"
    })
    private static void loadMimeTypes(Map<String, String> result, String resourceName) {
        try {
            Enumeration<URL> resources = NioHTTPD.class.getClassLoader().getResources(resourceName);
            while (resources.hasMoreElements()) {
                URL url = (URL) resources.nextElement();
                Properties properties = new Properties();
                InputStream stream = null;
                try {
                    stream = url.openStream();
                    properties.load(stream);
                } catch (IOException e) {
                    LOG.log(Level.SEVERE, "could not load mimetypes from " + url, e);
                } finally {
                    safeClose(stream);
                }
                result.putAll((Map) properties);
            }
        } catch (IOException e) {
            LOG.log(Level.INFO, "no mime types available at " + resourceName);
        }
    }

    public static String getMimeTypeForFile(String uri) {
        int dot = uri.lastIndexOf('.');
        String mime = null;
        if (dot >= 0) {
            mime = mimeTypes().get(uri.substring(dot + 1).toLowerCase());
        }
        return mime == null ? "application/octet-stream" : mime;
    }

    static final void safeClose(Object closeable) {
        try {
            if (closeable != null) {
                if (closeable instanceof Closeable) {
                    ((Closeable) closeable).close();
                } else if (closeable instanceof Socket) {
                    ((Socket) closeable).close();
                } else if (closeable instanceof SocketChannel) {
                    ((SocketChannel) closeable).close();
                } else if (closeable instanceof ServerSocketChannel) {
                    ((ServerSocketChannel) closeable).close();
                } else if (closeable instanceof InputStream) {
                    ((InputStream) closeable).close();
                } else if (closeable instanceof OutputStream) {
                    ((OutputStream) closeable).close();
                } else {
                    throw new IllegalArgumentException("Unknown object to close");
                }
            }
        } catch (IOException e) {
            NioHTTPD.LOG.log(Level.SEVERE, "Could not close", e);
        }
    }

    String hostname;

    int myPort;

    private volatile ServerSocketChannel myServerSocket;
    private volatile Selector selector;

    private ServerSocketFactory serverSocketFactory = new DefaultServerSocketFactory();

    private Thread myThread;

    /**
     * Pluggable strategy for asynchronously executing requests.
     */
    AsyncRunner asyncRunner;
    Map<SocketChannel, HTTPSession> callbackHandlerMap;

    /**
     * Pluggable strategy for creating and cleaning up temporary files.
     */
    TempFileManagerFactory tempFileManagerFactory;

    public NioHTTPD() {
        setTempFileManagerFactory(new DefaultTempFileManagerFactory());
//        setAsyncRunner(new DefaultAsyncRunner());
        callbackHandlerMap = new ConcurrentHashMap<>();
    }
//
//    static ClientHandler createClientHandler(NioHTTPD server, final SelectionKey selectionKey, final SocketChannel socketChannel) throws IOException {
//        return new ClientHandler(server, selectionKey, socketChannel);
//    }

    protected ServerRunnable createServerRunnable(NioHTTPD server, ServerSocketChannel socketChannel, Selector selector, final int timeout) {
        return new ServerRunnable(server, socketChannel, selector, timeout);
    }

    protected static Map<String, List<String>> decodeParameters(String queryString) {
        Map<String, List<String>> parms = new HashMap<String, List<String>>();
        if (queryString != null) {
            StringTokenizer st = new StringTokenizer(queryString, "&");
            while (st.hasMoreTokens()) {
                String e = st.nextToken();
                int sep = e.indexOf('=');
                String propertyName = sep >= 0 ? decodePercent(e.substring(0, sep)).trim() : decodePercent(e).trim();
                if (!parms.containsKey(propertyName)) {
                    parms.put(propertyName, new ArrayList<String>());
                }
                String propertyValue = sep >= 0 ? decodePercent(e.substring(sep + 1)) : null;
                if (propertyValue != null) {
                    parms.get(propertyName).add(propertyValue);
                }
            }
        }
        return parms;
    }

    static String decodePercent(String str) {
        String decoded = null;
        try {
            decoded = URLDecoder.decode(str, "UTF8");
        } catch (UnsupportedEncodingException ignored) {
            NioHTTPD.LOG.log(Level.WARNING, "Encoding not supported, ignored", ignored);
        }
        return decoded;
    }

    @SuppressWarnings("static-method")
    static boolean useGzipWhenAccepted(Response r) {
        return r.getMimeType() != null && (r.getMimeType().toLowerCase().contains("text/") || r.getMimeType().toLowerCase().contains("/json"));
    }

    public final int getListeningPort() {
        return -1;
    }

    public final boolean isAlive() {
        return wasStarted() && this.myServerSocket.isOpen() && this.myThread.isAlive();
    }

    public ServerSocketFactory getServerSocketFactory() {
        return serverSocketFactory;
    }

    public String getHostname() {
        return hostname;
    }

    public TempFileManagerFactory getTempFileManagerFactory() {
        return tempFileManagerFactory;
    }

    public Response serve(IHTTPSession session) {
        Map<String, String> files = new HashMap<String, String>();
        Method method = session.getMethod();
        if (Method.PUT.equals(method) || Method.POST.equals(method)) {
            try {
                session.parseBody(files);
            } catch (IOException ioe) {
                return new Response(session, Status.INTERNAL_ERROR, NioHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
            } catch (ResponseException re) {
                return new Response(session, re.getStatus(), NioHTTPD.MIME_PLAINTEXT, re.getMessage());
            }
        }

        Map<String, String> parms = session.getParams();
        parms.put(NioHTTPD.QUERY_STRING_PARAMETER, session.getQueryParameterString());
        return serve(session, session.getUri(), method, session.getHeaders(), parms, files);
    }

    @Deprecated
    public Response serve(IHTTPSession session, String uri, Method method, Map<String, String> headers, Map<String, String> parms, Map<String, String> files) {
        return new Response(session, Status.NOT_FOUND, NioHTTPD.MIME_PLAINTEXT, "Not Found");
    }

    public void setAsyncRunner(AsyncRunner asyncRunner) {
        this.asyncRunner = asyncRunner;
    }

    public void setTempFileManagerFactory(TempFileManagerFactory tempFileManagerFactory) {
        this.tempFileManagerFactory = tempFileManagerFactory;
    }

    public void serverStart(String hostname, int port) throws IOException {
        serverStart(hostname, port, NioHTTPD.SOCKET_READ_TIMEOUT);
    }

    public void serverStart(String hostname, int port, final int timeout) throws IOException {
        serverStart(hostname, port, timeout, true);
    }

    public void serverStart(String hostname, int port, final int timeout, boolean daemon) throws IOException {
        this.hostname = hostname;
        this.myPort = port;
        this.myServerSocket = this.getServerSocketFactory().create();
        this.myServerSocket.configureBlocking(false);
        this.selector = Selector.open();
        if(this.asyncRunner == null) {
            this.asyncRunner = new DefaultAsyncRunner(5, 5, 10, 30);
        }
        ServerRunnable serverRunnable = createServerRunnable(this, this.myServerSocket, this.selector, timeout);
        this.myThread = new Thread(serverRunnable);
        this.myThread.setDaemon(daemon);
        this.myThread.setName("NioHttpd Main Listener");
        this.myThread.start();
        /*this.myThread = new Thread(serverRunnable);
        this.myThread.setDaemon(daemon);
        this.myThread.setName("NanoHttpd Main Listener");
        this.myThread.start();
        while (!serverRunnable.hasBinded && serverRunnable.bindException == null) {
            try {
                Thread.sleep(10L);
            } catch (Throwable e) {
                // on android this may not be allowed, that's why we
                // catch throwable the wait should be very short because we are
                // just waiting for the bind of the socket
            }
        }
        if (serverRunnable.bindException != null) {
            throw serverRunnable.bindException;
        }*/
    }

    /**
     * Stop the server.
     */
    public void serverStop() {
        try {
            this.myServerSocket.close();
            this.selector.close();
            this.asyncRunner.closeAll();
//            if (this.myThread != null) {
//                this.myThread.join();
//            }
        } catch (Exception e) {
            NioHTTPD.LOG.log(Level.SEVERE, "Could not stop all connections", e);
        }
    }

    public final boolean wasStarted() {
        return this.myServerSocket != null && this.myThread != null;
    }

    public int getMyPort() {
        return myPort;
    }
}
